use rapier::counters::Counters;
use rapier::math::Real;

use crate::debug_render::DebugRenderPipelineResource;
use crate::harness::{Harness, RapierBroadPhaseType};
use crate::testbed::{
    PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR, RunMode, TestbedActionFlags,
    TestbedState, TestbedStateFlags,
};

pub use bevy_egui::egui;

use crate::PhysicsState;
use crate::settings::SettingValue;
use bevy_egui::EguiContexts;
use bevy_egui::egui::{ComboBox, Slider, Ui, Window};
use web_time::Instant;

#[cfg(feature = "dim3")]
use rapier::dynamics::FrictionModel;

pub(crate) fn update_ui(
    ui_context: &mut EguiContexts,
    state: &mut TestbedState,
    harness: &mut Harness,
    debug_render: &mut DebugRenderPipelineResource,
) {
    #[cfg(feature = "profiler_ui")]
    {
        profiler_ui(ui_context);
    }

    example_settings_ui(ui_context, state);

    Window::new("Parameters").show(ui_context.ctx_mut(), |ui| {
        if state.backend_names.len() > 1 && !state.example_names.is_empty() {
            let mut changed = false;
            ComboBox::from_label("backend")
                .width(150.0)
                .selected_text(state.backend_names[state.selected_backend])
                .show_ui(ui, |ui| {
                    for (id, name) in state.backend_names.iter().enumerate() {
                        changed = ui
                            .selectable_value(&mut state.selected_backend, id, *name)
                            .changed()
                            || changed;
                    }
                });

            if changed {
                state
                    .action_flags
                    .set(TestbedActionFlags::BACKEND_CHANGED, true);
            }

            ui.separator();
        }

        ui.horizontal(|ui| {
            if ui.button("<").clicked() && state.selected_example > 0 {
                state.selected_example -= 1;
                state
                    .action_flags
                    .set(TestbedActionFlags::EXAMPLE_CHANGED, true)
            }

            if ui.button(">").clicked() && state.selected_example + 1 < state.example_names.len() {
                state.selected_example += 1;
                state
                    .action_flags
                    .set(TestbedActionFlags::EXAMPLE_CHANGED, true)
            }

            let mut changed = false;
            egui::ComboBox::from_label("example")
                .width(150.0)
                .selected_text(state.example_names[state.selected_example])
                .show_ui(ui, |ui| {
                    for (id, name) in state.example_names.iter().enumerate() {
                        changed = ui
                            .selectable_value(&mut state.selected_example, id, *name)
                            .changed()
                            || changed;
                    }
                });
            if changed {
                state
                    .action_flags
                    .set(TestbedActionFlags::EXAMPLE_CHANGED, true);
            }
        });

        ui.separator();

        ui.collapsing("Scene infos", |ui| {
            scene_infos_ui(ui, &harness.physics);
        });
        ui.collapsing("Profile infos", |ui| {
            ui.horizontal_wrapped(|ui| {
                profiling_ui(ui, &harness.physics.pipeline.counters);
            });
        });
        ui.collapsing("Serialization infos", |ui| {
            ui.horizontal_wrapped(|ui| {
                ui.label(serialization_string(
                    harness.state.timestep_id,
                    &harness.physics,
                ))
            });
        });

        let integration_parameters = &mut harness.physics.integration_parameters;

        if state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
            || state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
        {
            ui.add(
                Slider::new(&mut integration_parameters.num_solver_iterations, 0..=10)
                    .text("pos. iters."),
            );
        } else {
            // Broad-phase.
            let mut changed = false;
            egui::ComboBox::from_label("broad-phase")
                .width(150.0)
                .selected_text(format!("{:?}", state.broad_phase_type))
                .show_ui(ui, |ui| {
                    let broad_phase_type = [
                        RapierBroadPhaseType::BvhSubtreeOptimizer,
                        RapierBroadPhaseType::BvhWithoutOptimization,
                    ];
                    for sty in broad_phase_type {
                        changed = ui
                            .selectable_value(&mut state.broad_phase_type, sty, format!("{sty:?}"))
                            .changed()
                            || changed;
                    }
                });

            if changed {
                harness.physics.broad_phase = state.broad_phase_type.init_broad_phase();
                // Restart the simulation after a broad-phase changes since some
                // broad-phase might not support hot-swapping.
                state.action_flags.set(TestbedActionFlags::RESTART, true);
            }

            // Friction model.
            #[cfg(feature = "dim3")]
            egui::ComboBox::from_label("friction-model")
                .width(150.0)
                .selected_text(format!("{:?}", integration_parameters.friction_model))
                .show_ui(ui, |ui| {
                    let friction_model = [FrictionModel::Simplified, FrictionModel::Coulomb];
                    for model in friction_model {
                        changed = ui
                            .selectable_value(
                                &mut integration_parameters.friction_model,
                                model,
                                format!("{model:?}"),
                            )
                            .changed()
                            || changed;
                    }
                });

            // Solver iterations.
            ui.add(
                Slider::new(&mut integration_parameters.num_solver_iterations, 0..=10)
                    .text("num solver iters."),
            );
            ui.add(
                Slider::new(
                    &mut integration_parameters.num_internal_pgs_iterations,
                    1..=40,
                )
                .text("num internal PGS iters."),
            );
            ui.add(
                Slider::new(
                    &mut integration_parameters.num_internal_stabilization_iterations,
                    0..=100,
                )
                .text("max internal stabilization iters."),
            );
            ui.add(
                Slider::new(&mut integration_parameters.warmstart_coefficient, 0.0..=1.0)
                    .text("warmstart coefficient"),
            );

            let mut substep_params = *integration_parameters;
            substep_params.dt /= substep_params.num_solver_iterations as Real;
            let curr_erp = substep_params.contact_softness.erp(substep_params.dt);
            let curr_cfm_factor = substep_params
                .contact_softness
                .cfm_factor(substep_params.dt);
            ui.add(
                Slider::new(
                    &mut integration_parameters.contact_softness.natural_frequency,
                    0.01..=120.0,
                )
                .text(format!("contacts Hz (erp = {curr_erp:.3})")),
            );
            ui.add(
                Slider::new(
                    &mut integration_parameters.contact_softness.damping_ratio,
                    0.01..=20.0,
                )
                .text(format!("damping ratio (cfm-factor = {curr_cfm_factor:.3})",)),
            );
        }

        #[cfg(feature = "parallel")]
        {
            let mut num_threads = harness.state.num_threads();
            ui.add(
                Slider::new(&mut num_threads, 1..=num_cpus::get_physical()).text("num. threads"),
            );
            harness.state.set_num_threads(num_threads);
        }
        ui.add(
            Slider::new(&mut integration_parameters.max_ccd_substeps, 0..=10).text("CCD substeps"),
        );
        ui.add(
            Slider::new(&mut integration_parameters.min_island_size, 1..=10_000)
                .text("min island size"),
        );
        ui.add(Slider::new(&mut state.nsteps, 1..=100).text("sims. per frame"));

        let mut frequency = integration_parameters.inv_dt().round() as u32;
        ui.add(Slider::new(&mut frequency, 0..=240).text("frequency (Hz)"));
        let mut gravity_y = harness.physics.gravity.y;
        if ui
            .add(Slider::new(&mut gravity_y, 0.0..=-200.0).text("Gravity"))
            .changed()
        {
            harness.physics.gravity.y = gravity_y;
        }
        integration_parameters.set_inv_dt(frequency as Real);

        let mut sleep = state.flags.contains(TestbedStateFlags::SLEEP);
        let mut draw_surfaces = state.flags.contains(TestbedStateFlags::DRAW_SURFACES);
        // let mut contact_points = state.flags.contains(TestbedStateFlags::CONTACT_POINTS);
        // let mut wireframe = state.flags.contains(TestbedStateFlags::WIREFRAME);
        ui.checkbox(&mut sleep, "sleep enabled");
        // ui.checkbox(&mut contact_points, "draw contacts");
        // ui.checkbox(&mut wireframe, "draw wireframes");
        ui.checkbox(&mut draw_surfaces, "surface render enabled");
        ui.checkbox(&mut debug_render.enabled, "debug render enabled");

        state.flags.set(TestbedStateFlags::SLEEP, sleep);
        state
            .flags
            .set(TestbedStateFlags::DRAW_SURFACES, draw_surfaces);
        // state
        //     .flags
        //     .set(TestbedStateFlags::CONTACT_POINTS, contact_points);
        // state.flags.set(TestbedStateFlags::WIREFRAME, wireframe);
        ui.separator();
        let label = if state.running == RunMode::Stop {
            "Start (T)"
        } else {
            "Pause (T)"
        };

        if ui.button(label).clicked() {
            if state.running == RunMode::Stop {
                state.running = RunMode::Running
            } else {
                state.running = RunMode::Stop
            }
        }

        if ui.button("Single Step (S)").clicked() {
            state.running = RunMode::Step;
        }

        if ui.button("Take snapshot").clicked() {
            state
                .action_flags
                .set(TestbedActionFlags::TAKE_SNAPSHOT, true);
        }

        if ui.button("Restore snapshot").clicked() {
            state
                .action_flags
                .set(TestbedActionFlags::RESTORE_SNAPSHOT, true);
        }

        if ui.button("Restart (R)").clicked() {
            state.action_flags.set(TestbedActionFlags::RESTART, true);
        }
    });
}

fn scene_infos_ui(ui: &mut Ui, physics: &PhysicsState) {
    ui.label(format!("# rigid-bodies: {}", physics.bodies.len()));
    ui.label(format!("# colliders: {}", physics.colliders.len()));
    ui.label(format!("# impulse joint: {}", physics.impulse_joints.len()));
    // ui.label(format!(
    //     "# multibody joint: {}",
    //     physics.multibody_joints.len()
    // ));
}

fn profiling_ui(ui: &mut Ui, counters: &Counters) {
    egui::CollapsingHeader::new(format!(
        "Total: {:.2}ms - {} fps",
        counters.step_time_ms(),
        (1000.0 / counters.step_time_ms()).round()
    ))
    .id_salt("total")
    .show(ui, |ui| {
        egui::CollapsingHeader::new(format!(
            "Collision detection: {:.2}ms",
            counters.collision_detection_time_ms()
        ))
        .id_salt("collision detection")
        .show(ui, |ui| {
            ui.label(format!(
                "Broad-phase: {:.2}ms",
                counters.broad_phase_time_ms()
            ));
            ui.label(format!(
                "Narrow-phase: {:.2}ms",
                counters.narrow_phase_time_ms()
            ));
        });
        egui::CollapsingHeader::new(format!("Solver: {:.2}ms", counters.solver_time_ms()))
            .id_salt("solver")
            .show(ui, |ui| {
                ui.label(format!(
                    "Velocity assembly: {:.2}ms",
                    counters.solver.velocity_assembly_time.time_ms()
                ));
                ui.label(format!(
                    "Velocity resolution: {:.2}ms",
                    counters.velocity_resolution_time_ms()
                ));
                ui.label(format!(
                    "Velocity integration: {:.2}ms",
                    counters.solver.velocity_update_time.time_ms()
                ));
                ui.label(format!(
                    "Writeback: {:.2}ms",
                    counters.solver.velocity_writeback_time.time_ms()
                ));
            });
        egui::CollapsingHeader::new(format!("CCD: {:.2}ms", counters.ccd_time_ms()))
            .id_salt("ccd")
            .show(ui, |ui| {
                ui.label(format!("# of substeps: {}", counters.ccd.num_substeps));
                ui.label(format!(
                    "TOI computation: {:.2}ms",
                    counters.ccd.toi_computation_time.time_ms(),
                ));
                ui.label(format!(
                    "Broad-phase: {:.2}ms",
                    counters.ccd.broad_phase_time.time_ms()
                ));
                ui.label(format!(
                    "Narrow-phase: {:.2}ms",
                    counters.ccd.narrow_phase_time.time_ms(),
                ));
                ui.label(format!(
                    "Solver: {:.2}ms",
                    counters.ccd.solver_time.time_ms()
                ));
            });
        ui.label(format!(
            "Island computation: {:.2}ms",
            counters.island_construction_time_ms()
        ));
        ui.label(format!(
            "User changes: {:.2}ms",
            counters.stages.user_changes.time_ms()
        ));
    });
}

fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String {
    let t = Instant::now();
    // let t = Instant::now();
    let bf = bincode::serialize(&physics.broad_phase).unwrap();
    // println!("bf: {}", Instant::now() - t);
    // let t = Instant::now();
    let nf = bincode::serialize(&physics.narrow_phase).unwrap();
    // println!("nf: {}", Instant::now() - t);
    // let t = Instant::now();
    let bs = bincode::serialize(&physics.bodies).unwrap();
    // println!("bs: {}", Instant::now() - t);
    // let t = Instant::now();
    let cs = bincode::serialize(&physics.colliders).unwrap();
    // println!("cs: {}", Instant::now() - t);
    // let t = Instant::now();
    let js = bincode::serialize(&physics.impulse_joints).unwrap();
    // println!("js: {}", Instant::now() - t);
    let serialization_time = Instant::now() - t;
    let hash_bf = md5::compute(&bf);
    let hash_nf = md5::compute(&nf);
    let hash_bodies = md5::compute(&bs);
    let hash_colliders = md5::compute(&cs);
    let hash_joints = md5::compute(&js);
    format!(
        r#"Serialization time: {:.2}ms
Hashes at frame: {}
|_ Broad phase [{:.1}KB]: {}
|_ Narrow phase [{:.1}KB]: {}
|_ &RigidBodySet [{:.1}KB]: {}
|_ Colliders [{:.1}KB]: {}
|_ Joints [{:.1}KB]: {}"#,
        serialization_time.as_secs_f64() * 1000.0,
        timestep_id,
        bf.len() as f32 / 1000.0,
        format!("{hash_bf:?}").split_at(10).0,
        nf.len() as f32 / 1000.0,
        format!("{hash_nf:?}").split_at(10).0,
        bs.len() as f32 / 1000.0,
        format!("{hash_bodies:?}").split_at(10).0,
        cs.len() as f32 / 1000.0,
        format!("{hash_colliders:?}").split_at(10).0,
        js.len() as f32 / 1000.0,
        format!("{hash_joints:?}").split_at(10).0,
    )
}

fn example_settings_ui(ui_context: &mut EguiContexts, state: &mut TestbedState) {
    if state.example_settings.is_empty() {
        // Don’t show any window if there is no settings for the
        // example.
        return;
    }

    Window::new("Example settings").show(ui_context.ctx_mut(), |ui| {
        let mut any_changed = false;
        for (name, value) in state.example_settings.iter_mut() {
            let prev_value = value.clone();
            match value {
                SettingValue::Label(value) => {
                    ui.label(format!("{name}: {value}"));
                }
                SettingValue::F32 { value, range } => {
                    ui.add(Slider::new(value, range.clone()).text(name));
                }
                SettingValue::U32 { value, range } => {
                    ui.horizontal(|ui| {
                        if ui.button("<").clicked() && *value > *range.start() {
                            *value -= 1;
                        }
                        if ui.button(">").clicked() && *value <= *range.end() {
                            *value += 1;
                        }

                        ui.add(Slider::new(value, range.clone()).text(name));
                    });
                }
                SettingValue::Bool { value } => {
                    ui.checkbox(value, name);
                }
                SettingValue::String { value, range } => {
                    ComboBox::from_label(name)
                        .width(150.0)
                        .selected_text(&range[*value])
                        .show_ui(ui, |ui| {
                            for (id, name) in range.iter().enumerate() {
                                ui.selectable_value(value, id, name);
                            }
                        });
                }
            }

            any_changed = any_changed || *value != prev_value;
        }

        if any_changed {
            // The value changed, request a restart.
            state.action_flags.set(TestbedActionFlags::RESTART, true);
        }
    });
}

#[cfg(feature = "profiler_ui")]
fn profiler_ui(ui_context: &mut EguiContexts) {
    let window = egui::Window::new("Profiling");
    let window = window.default_open(false);

    #[cfg(feature = "unstable-puffin-pr-235")]
    {
        use std::sync::Once;
        static START: Once = Once::new();

        fn set_default_rapier_filter() {
            let mut profile_ui = puffin_egui::PROFILE_UI.lock();
            profile_ui
                .profiler_ui
                .flamegraph_options
                .scope_name_filter
                .set_filter("Harness::step_with_graphics".to_string());
        }
        START.call_once(|| {
            set_default_rapier_filter();
        });
        window.show(ui_context.ctx_mut(), |ui| {
            if ui.button("🔍 Rapier filter").clicked() {
                set_default_rapier_filter();
            }
            puffin_egui::profiler_ui(ui);
        });
    }

    #[cfg(not(feature = "unstable-puffin-pr-235"))]
    window.show(ui_context.ctx_mut(), |ui| {
        puffin_egui::profiler_ui(ui);
    });
}
